#!/usr/bin/python3

# This file is part of Cockpit.
#
# Copyright (C) 2013 Red Hat, Inc.
#
# Cockpit is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 2.1 of the License, or
# (at your option) any later version.
#
# Cockpit is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.

import json
import subprocess

import parent
from testlib import *
from hacklib import *


class DashBoardHelpers:

    def check_discovered_addresses(self, b, addresses):
        b.click('#dashboard-add')
        b.wait_popup('dashboard_setup_server_dialog')
        self.wait_discovered_addresses(b, addresses)
        b.click('#dashboard_setup_server_dialog .btn-default')
        b.wait_popdown('dashboard_setup_server_dialog')

    def wait_discovered_addresses(self, b, expected):
        b.wait_js_func(
            """(function(expected) {
                var nodes = document.querySelectorAll('#dashboard_setup_server_dialog datalist option');
                var actual = Array.prototype.map.call(nodes, function(e) {
                    return e.getAttribute("value");
                });
                return expected.sort().toString() == actual.sort().toString();
                })""", expected)

    def wait_dashboard_addresses(self, b, expected):
        b.wait_js_func(
            """(function(expected) {
                var nodes = document.querySelectorAll('#dashboard-hosts .list-group-item');
                var addresses = Array.prototype.map.call(nodes, function(e) {
                    return e.getAttribute("data-address");
                });
                return expected.sort().toString() == addresses.sort().toString();
            })""", expected)

    def machine_remove(self, b, address):
        b.click("#dashboard-enable-edit")
        b.click("#dashboard-hosts a[data-address='%s'] button.pficon-delete" % address)

    def add_machine(self, b, address, known_host=False):
        b.click('#dashboard-add')
        b.wait_popup('dashboard_setup_server_dialog')
        b.set_val('#add-machine-address', address)
        self.add_machine_finish(b, known_host=known_host)

    def add_machine_finish(self, b, known_host=False):
        b.wait_text('#dashboard_setup_server_dialog .btn-primary', "Add")
        b.click('#dashboard_setup_server_dialog .btn-primary')
        if not known_host:
            b.wait_in_text('#dashboard_setup_server_dialog', "Fingerprint")
            b.click('#dashboard_setup_server_dialog .btn-primary')
        b.wait_popdown('dashboard_setup_server_dialog')


@skipBrowser("Firefox looses track of contexts", "firefox")
class TestBasicDashboard(MachineCase, DashBoardHelpers):
    provision = {
        'machine1': {"address": "10.111.113.1/20"},
        'machine2': {"address": "10.111.113.2/20"},
        'machine3': {"address": "10.111.113.3/20"}
    }

    def setUp(self):
        super().setUp()
        HACK_disable_problematic_preloads(self.machines["machine2"])
        HACK_disable_problematic_preloads(self.machines["machine3"])

    def testBasic(self):
        b = self.browser
        m = self.machine

        self.login_and_go("/dashboard")

        self.wait_dashboard_addresses(b, ["localhost"])

        # Start second browser and check that it is in sync
        b2 = self.new_browser()
        b2.default_user = "admin"
        b2.login_and_go("/dashboard")
        self.wait_dashboard_addresses(b2, ["localhost"])
        b.wait_present("#dashboard-hosts a[data-address='localhost'] button.pficon-delete.disabled")

        self.add_machine(b, "10.111.113.2")
        self.wait_dashboard_addresses(b, ["localhost", "10.111.113.2"])
        self.wait_dashboard_addresses(b2, ["localhost", "10.111.113.2"])

        # Should show successful state and icon
        b.wait_attr("#dashboard-hosts a.connected[data-address='10.111.113.2'] img",
                    'src', '../shell/images/server-small.png')
        b2.wait_attr("#dashboard-hosts a.connected[data-address='10.111.113.2'] img",
                     'src', '../shell/images/server-small.png')

        self.add_machine(b, "10.111.113.3")
        self.wait_dashboard_addresses(b, ["localhost", "10.111.113.3", "10.111.113.2"])
        self.wait_dashboard_addresses(b2, ["localhost", "10.111.113.3", "10.111.113.2"])

        # Remove two
        self.machine_remove(b, "10.111.113.2")
        self.wait_dashboard_addresses(b, ["localhost", "10.111.113.3"])
        self.wait_dashboard_addresses(b2, ["localhost", "10.111.113.3"])

        self.machine_remove(b, "10.111.113.3")
        self.wait_dashboard_addresses(b, ["localhost"])
        self.wait_dashboard_addresses(b2, ["localhost"])

        # Check that the two removed machines are listed in "Add Host"
        # on both browsers
        self.check_discovered_addresses(b, ["10.111.113.2", "10.111.113.3"])
        self.check_discovered_addresses(b2, ["10.111.113.2", "10.111.113.3"])

        # Add one back, check addresses on both browsers
        self.add_machine(b, "10.111.113.2", True)
        self.wait_dashboard_addresses(b, ["localhost", "10.111.113.2"])
        self.wait_dashboard_addresses(b2, ["localhost", "10.111.113.2"])
        self.check_discovered_addresses(b, ["10.111.113.3"])
        self.check_discovered_addresses(b2, ["10.111.113.3"])

        # Should show successful state and icon again
        b.wait_attr("#dashboard-hosts a.connected[data-address='10.111.113.2'] img",
                    'src', '../shell/images/server-small.png')
        b2.wait_attr("#dashboard-hosts a.connected[data-address='10.111.113.2'] img",
                     'src', '../shell/images/server-small.png')

        # And the second one, check addresses on both browsers
        self.add_machine(b, "10.111.113.3", True)
        self.wait_dashboard_addresses(b, ["localhost", "10.111.113.2", "10.111.113.3"])
        self.wait_dashboard_addresses(b2, ["localhost", "10.111.113.2", "10.111.113.3"])
        self.check_discovered_addresses(b, [])
        self.check_discovered_addresses(b2, [])

        # Move m2 known host key to ~/.ssh, verify that it's known
        self.machine_remove(b, "10.111.113.2")
        self.wait_dashboard_addresses(b, ["localhost", "10.111.113.3"])
        key = m.execute("grep '10.111.113.2' /etc/ssh/ssh_known_hosts && sed -i '/10.111.113.2/d' /etc/ssh/ssh_known_hosts")
        m.execute(
            "mkdir -p ~admin/.ssh && echo '{0}' > ~admin/.ssh/known_hosts && chown -R admin:admin ~admin/.ssh""".format(key))
        self.add_machine(b, "10.111.113.2", True)
        self.wait_dashboard_addresses(b, ["localhost", "10.111.113.2", "10.111.113.3"])
        # Should be connected, no error
        b.wait_attr("#dashboard-hosts a.connected[data-address='10.111.113.2'] img",
                    'src', '../shell/images/server-small.png')

        # Test change user, not doing in edit to reuse machines

        # Navigate to load iframe
        b.click("#dashboard-hosts .list-group-item[data-address='10.111.113.3']")
        b.enter_page("/system", "10.111.113.3")

        b.switch_to_top()
        b.go("/dashboard")
        b.enter_page("/dashboard")
        b.switch_to_top()
        b.wait_present("iframe.container-frame[name='cockpit1:10.111.113.3/system']")
        b.enter_page("/dashboard")

        b.click('#dashboard-enable-edit')

        b.click("#dashboard-hosts .list-group-item[data-address='10.111.113.3'] button.pficon-edit")

        b.wait_popup('host-edit-dialog')
        b.set_val('#host-edit-user', 'bad-user')

        b.click('#host-edit-apply')
        b.wait_popdown('host-edit-dialog')
        b.wait_not_present("iframe.container-frame[name='cockpit1:10.111.113.3/system']")
        b.wait_present("#dashboard-hosts .failed[data-address='10.111.113.3']")

        b.click('#dashboard-enable-edit')
        b.click("#dashboard-hosts .list-group-item[data-address='10.111.113.3'] button.pficon-edit")
        b.wait_popup('host-edit-dialog')
        b.set_val('#host-edit-user', '')
        b.click('#host-edit-apply')
        b.wait_popdown('host-edit-dialog')
        b.wait_present("#dashboard-hosts .connected[data-address='10.111.113.3']")

        # Test switching
        b.leave_page()
        b.wait_js_cond('ph_select("#machine-dropdown ul li").length == 3')

        b.click("#host-nav-link")

        b.click("#machine-link")
        b.click("#machine-dropdown ul a[data-address='localhost']")
        b.wait_js_cond('window.location.pathname == "/system"')
        b.click("#machine-link")
        b.click("#machine-dropdown ul a[data-address='10.111.113.2']")
        b.wait_js_cond('window.location.pathname.indexOf("/@10.111.113.2") === 0')
        b.click("#machine-link")
        b.click("#machine-dropdown ul a[data-address='10.111.113.3']")
        b.wait_js_cond('window.location.pathname.indexOf("/@10.111.113.3") === 0')

        b.enter_page("/system", "10.111.113.3")
        b.wait_text_not("#system_information_systime_button", "")
        b.click(".system-usage a")  # View graphs
        b.enter_page("/system/graphs", "10.111.113.3")
        b.click("#link-network a")
        b.enter_page("/network", "10.111.113.3")

        self.allow_hostkey_messages()
        self.allow_journal_messages(".*server offered unsupported authentication methods: password public-key.*")


class TestDashboardSetup(MachineCase, DashBoardHelpers):
    provision = {
        'machine1': {"address": "10.111.113.1/20"},
        'machine2': {"address": "10.111.113.2/20"},
        'machine3': {"address": "10.111.113.3/20"}
    }

    def testSetup(self):
        b = self.browser

        m1 = self.machine
        m2 = self.machines['machine2']

        # lockout admin
        m2.execute("echo admin:badpass | chpasswd")
        # Logging in as root is no longer allowed by default by sshd
        m2.execute("sed -ri 's/#?PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config")
        m2.execute("systemctl restart sshd")

        # Create some users on m1 and m2.
        m1.execute("getent group docker >/dev/null || groupadd docker")
        m2.execute("getent group docker >/dev/null || groupadd docker")

        m1.execute("useradd junior -G docker")
        m1.execute("echo junior:foobar | chpasswd")
        m1.execute("useradd nosync -G docker")
        m1.execute("echo nosync:foobar | chpasswd")

        m1.execute("useradd senior -G %s" % m1.get_admin_group())
        m1.execute("echo senior:foobar | chpasswd")
        m2.execute("useradd senior")
        m2.execute("echo senior:barfoo | chpasswd")

        # Sync them via Setup.

        self.login_and_go("/dashboard")

        self.wait_dashboard_addresses(b, ["localhost"])
        b.click('#dashboard-add')
        b.wait_popup('dashboard_setup_server_dialog')
        b.set_val('#add-machine-address', "10.111.113.2")
        b.wait_text('#dashboard_setup_server_dialog .btn-primary', "Add")
        b.click('#dashboard_setup_server_dialog .btn-primary')
        b.wait_in_text('#dashboard_setup_server_dialog', "Fingerprint")
        b.click('#dashboard_setup_server_dialog .btn-primary')
        b.wait_in_text('#dashboard_setup_server_dialog .modal-title', "Log in to")
        b.click("#do-sync-users")

        b.wait_text('#dashboard_setup_server_dialog .btn-primary', "Synchronize")
        b.wait_in_text("#sync-users", "admin")
        b.wait_present("#sync-username")

        b.click('#sync-users input[name="admin"]')
        b.click('#sync-users input[name="junior"]')
        b.click('#sync-users input[name="senior"]')

        b.set_val('#sync-username', "root")
        b.set_val('#sync-password', "notthepassword")
        b.click('#dashboard_setup_server_dialog .btn-primary')
        b.wait_text('#dashboard_setup_server_dialog .dialog-error', "Login failed")
        b.set_val('#sync-password', "foobar")

        b.click('#dashboard_setup_server_dialog .btn-primary')
        b.wait_popdown('dashboard_setup_server_dialog')

        # Check the result
        def password_hash(machine, user):
            return machine.execute("getent shadow %s | cut -d: -f2" % user)

        def groups(machine, user):
            return machine.execute("groups %s | cut -d: -f2" % user)

        self.assertEqual(password_hash(m1, "junior"),
                         password_hash(m2, "junior"))

        self.assertEqual(password_hash(m1, "senior"),
                         password_hash(m2, "senior"))

        self.assertEqual(password_hash(m1, "admin"),
                         password_hash(m2, "admin"))

        self.assertIn("docker", groups(m2, "junior"))
        self.assertIn(m2.get_admin_group(), groups(m2, "senior"))

        self.assertRaises(subprocess.CalledProcessError, m2.execute, "id nosync")

        m2.execute("ps aux | grep cockpit-bridge | grep admin")
        self.allow_hostkey_messages()


class TestDashboardEditLimits(MachineCase, DashBoardHelpers):

    def testEdit(self):
        b = self.browser
        m = self.machine

        self.login_and_go("/dashboard")
        self.wait_dashboard_addresses(b, ["localhost"])
        old_style = b.attr("#dashboard-hosts .list-group-item[data-address='localhost']", "style")

        b.wait_not_visible('#dashboard-hosts a:first-child button.pficon-edit')
        b.click('#dashboard-enable-edit')

        b.click('#dashboard-hosts a:first-child button.pficon-edit')
        b.wait_not_visible('#dashboard-hosts a:first-child button.pficon-edit')

        b.wait_popup('host-edit-dialog')
        b.set_val('#host-edit-name', "Horst")
        b.click('#host-edit-color')
        b.wait_visible('#host-edit-color-popover')
        b.wait_visible('#host-edit-user[disabled]')
        b.click('#host-edit-color-popover div.popover-content > div:first-child > div:nth-child(3)')
        b.wait_not_visible('#host-edit-color-popover')
        b.click('#host-edit-apply')
        b.wait_popdown('host-edit-dialog')

        b.wait_in_text('#dashboard-hosts a:first-child span.host-label', "Horst")
        b.wait_not_attr("#dashboard-hosts .list-group-item[data-address='localhost']", "style", old_style)
        self.assertEqual(m.execute("hostnamectl --pretty"), "Horst\n")

    def testLimits(self):
        b = self.browser
        m = self.machine

        def fake_machines(amount):
            # build a machine json manually
            d = {
                "localhost": {"visible": True, "address": "localhost"}
            }

            for i in range(amount):
                n = "bad{0}".format(i)
                d[n] = {"visible": True, "address": n}

            m.execute("echo '{0}' > /etc/cockpit/machines.d/99-webui.json".format(json.dumps(d)))
            return list(d.keys())

        def check_limit(limit):
            b.click('#dashboard-add')
            b.wait_popup('dashboard_setup_server_dialog')
            if limit:
                b.wait_in_text("#dashboard_setup_server_dialog .dashboard-machine-warning",
                               "{0} machines".format(limit))
            else:
                b.wait_not_present("#dashboard_setup_server_dialog .dashboard-machine-warning")

            b.click("#dashboard_setup_server_dialog .btn-default")

        self.login_and_go("/dashboard")

        self.wait_dashboard_addresses(b, ["localhost"])
        check_limit(0)

        self.wait_dashboard_addresses(b, fake_machines(3))
        check_limit(0)

        self.wait_dashboard_addresses(b, fake_machines(14))
        check_limit(20)
        self.allow_journal_messages(
            ".*couldn't connect: Failed to resolve hostname bad.*",
            ".*refusing to connect to unknown host.*"
        )


if __name__ == '__main__':
    test_main()
